﻿using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Swimj.Core.Converter
{
    public class JsonDictionaryConverter : JsonConverter
    {
        public static JObject DictionaryToJObject(IDictionary<string, object> dictionary)
        {
            var result = new JObject();
            foreach (var (key, value) in dictionary)
            {
                if (value is IDictionary<string, object> dict)
                    result.Add(key, DictionaryToJObject(dict));
                else if (value is IEnumerable<object> list)
                    result.Add(key, new JArray(list));
                else
                    result.Add(key, JToken.FromObject(value));
            }

            return result;
        }

        public static Dictionary<string, string> Flatten(object obj)
        {
            return Flatten(JObject.FromObject(obj));
        }

        public static Dictionary<string, string> Flatten(JObject jsonObject)
        {
            IEnumerable<JToken> jTokens = jsonObject.Descendants().Where(p => !p.Any());
            Dictionary<string, string> results = jTokens.Aggregate(new Dictionary<string, string>(), (properties, jToken) =>
            {
                properties.Add(jToken.Path, jToken.ToString());
                return properties;
            });
            return results;
        }

        public static IDictionary<string, object>? ConvertToDictionary(JObject? jObject)
        {
            return jObject?.ToObject<IDictionary<string, object>>(new JsonSerializer { Converters = { new JsonDictionaryConverter() } });
        }

        public static IDictionary<string, object>? ConvertToUnflattenDictionary(IDictionary<string, string> flatDictionary)
        {
            return ConvertToDictionary(Unflatten(flatDictionary));
        }

        public static JObject? Unflatten(IDictionary<string, string> keyValues)
        {
            JContainer? result = null;
            JsonMergeSettings setting = new() { MergeArrayHandling = MergeArrayHandling.Merge };
            foreach (var pathValue in keyValues)
            {
                if (result == null)
                {
                    result = UnflattenSingle(pathValue);
                }
                else
                {
                    var content = UnflattenSingle(pathValue);
                    if (content != null)
                        result.Merge(content, setting);
                }
            }
            return result as JObject;
        }

        public override bool CanConvert(Type objectType) { return typeof(IDictionary<string, object>).IsAssignableFrom(objectType); }

        public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
        {
            WriteValue(writer, value);
        }

        public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
        {
            return ReadValue(reader);
        }

        private void WriteValue(JsonWriter writer, object? value)
        {
            if (value != null)
            {
                var t = JToken.FromObject(value);
                switch (t.Type)
                {
                    case JTokenType.Object:
                        WriteObject(writer, value);
                        break;
                    case JTokenType.Array:
                        WriteArray(writer, value);
                        break;
                    default:
                        writer.WriteValue(value);
                        break;
                }
            }
        }

        private void WriteObject(JsonWriter writer, object value)
        {
            writer.WriteStartObject();
            if (value is IDictionary<string, object> obj)
            {
                foreach (var kvp in obj)
                {
                    writer.WritePropertyName(kvp.Key);
                    WriteValue(writer, kvp.Value);
                }
            }
            writer.WriteEndObject();
        }

        private void WriteArray(JsonWriter writer, object value)
        {
            writer.WriteStartArray();
            var array = value as IEnumerable<object> ?? Enumerable.Empty<object>();
            foreach (var o in array)
            {
                WriteValue(writer, o);
            }
            writer.WriteEndArray();
        }

        private object? ReadValue(JsonReader reader)
        {
            while (reader.TokenType == JsonToken.Comment)
            {
                if (!reader.Read()) throw new JsonSerializationException("Unexpected Token when converting IDictionary<string, object>");
            }

            switch (reader.TokenType)
            {
                case JsonToken.StartObject:
                    return ReadObject(reader);
                case JsonToken.StartArray:
                    return this.ReadArray(reader);
                case JsonToken.Integer:
                case JsonToken.Float:
                case JsonToken.String:
                case JsonToken.Boolean:
                case JsonToken.Undefined:
                case JsonToken.Null:
                case JsonToken.Date:
                case JsonToken.Bytes:
                    return reader.Value;
                default:
                    throw new JsonSerializationException
                        ($"Unexpected token when converting IDictionary<string, object>: {reader.TokenType}");
            }
        }

        private object ReadArray(JsonReader reader)
        {
            IList<object> list = new List<object>();

            while (reader.Read())
            {
                switch (reader.TokenType)
                {
                    case JsonToken.Comment:
                        break;
                    default:
                        var v = ReadValue(reader);
                        if (v != null)
                            list.Add(v);
                        break;
                    case JsonToken.EndArray:
                        return list;
                }
            }

            throw new JsonSerializationException("Unexpected end when reading IDictionary<string, object>");
        }

        private object ReadObject(JsonReader reader)
        {
            var obj = new Dictionary<string, object>();

            while (reader.Read())
            {
                switch (reader.TokenType)
                {
                    case JsonToken.PropertyName:
                        var propertyName = reader.Value?.ToString();

                        if (!reader.Read())
                        {
                            throw new JsonSerializationException("Unexpected end when reading IDictionary<string, object>");
                        }

                        var v = ReadValue(reader);

                        if (!string.IsNullOrEmpty(propertyName) && v != null)
                            obj[propertyName] = v;
                        break;
                    case JsonToken.Comment:
                        break;
                    case JsonToken.EndObject:
                        return obj;
                }
            }

            throw new JsonSerializationException("Unexpected end when reading IDictionary<string, object>");
        }

        private static JContainer? UnflattenSingle(KeyValuePair<string, string> keyValue)
        {
            string path = keyValue.Key;
            string value = keyValue.Value;
            var pathSegments = SplitPath(path);

            JContainer? lastItem = null;
            foreach (var pathSegment in pathSegments.Reverse())
            {
                var type = GetJsonTokenType(pathSegment);
                switch (type)
                {
                    case JTokenType.Object:
                        var obj = new JObject();
                        if (null == lastItem)
                        {
                            obj.Add(pathSegment, value);
                        }
                        else
                        {
                            obj.Add(pathSegment, lastItem);
                        }
                        lastItem = obj;
                        break;
                    case JTokenType.Array:
                        var array = new JArray();
                        int index = GetArrayIndex(pathSegment);
                        array = FillEmpty(array, index);
                        if (lastItem == null)
                        {
                            array[index] = value;
                        }
                        else
                        {
                            array[index] = lastItem;
                        }
                        lastItem = array;
                        break;
                    default:
                        throw new ArgumentOutOfRangeException(nameof(type), type, "");
                }
            }
            return lastItem;
        }

        public static IList<string> SplitPath(string path)
        {
            IList<string> result = new List<string>();
            Regex reg = new Regex(@"(?!\.)([^. ^\[\]]+)|(?!\[)(\d+)(?=\])");
            foreach (Match match in reg.Matches(path))
            {
                result.Add(match.Value);
            }
            return result;
        }

        private static JArray FillEmpty(JArray array, int index)
        {
            for (int i = 0; i <= index; i++)
            {
                array.Add(null);
            }
            return array;
        }

        private static JTokenType? GetJsonTokenType(string pathSegment)
        {
            return int.TryParse(pathSegment, out _) ? JTokenType.Array : JTokenType.Object;
        }

        private static int GetArrayIndex(string pathSegment)
        {
            if (int.TryParse(pathSegment, out var result))
            {
                return result;
            }
            throw new Exception("Unable to parse array index: " + pathSegment);
        }
    }
}